/**
 * @file DatabaseResource.java
 * @author ApertureScience
 */
package aperturescience.resources;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Iterator;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.restlet.Client;
import org.restlet.data.Protocol;
import org.restlet.ext.json.JsonRepresentation;
import org.restlet.representation.Representation;
import org.restlet.resource.ClientResource;
import org.restlet.resource.ResourceException;

import aperturescience.types.Action;
import aperturescience.types.CouchDBObject;
import aperturescience.types.JSONPatient;

/**
 * This class has direct access to our CouchDB database and handles all CRUD (Create, Retrieve, Update, and Delete)
 * requests coming in from clients
 */
public class DatabaseResource
{
	private final String databaseURL;			// URL to database on our server
	private String databaseName;				// name of database
	private ClientResource databaseConnection;	// database object; sends all CRUD requests directly to server
	private Representation dataRep;				// temporary value: stores response to a request made to the database
	private JSONObject jsonObject;				// the JSON Object obtained from request
	
	/**
	 * @constructor DatabaseResource
	 * Sets up connection to database
	 *
	 * @param database_URL		URL to couchDb database on server.
	 * @param database_name		Name of the couchDb database.
	 */
	public DatabaseResource(String database_URL, String database_name) {
		databaseURL = database_URL;
		databaseConnection = new ClientResource(databaseURL);
		this.databaseName = database_name;
		databaseConnection.setNext(new Client(Protocol.HTTP));
	}
	
	public String createAction(Action action)
	throws ResourceException {
		return createEntry(action);
	}

	/**
	 * Creates a patient in the database.
	 * 
	 * @param patient The JSONPatient to create in the database.
	 * @return patientId The patient id number if successful, null otherwise.
	 * 
	 * Warning: if there is an error in the database, there is no verification/recovery process in place
	 */
	public String createPatient(JSONPatient patient)
	throws ResourceException {
		return createEntry(patient);
	}
	
	private String createEntry( CouchDBObject data )
	throws ResourceException {
		int count = incrementCount(data.getIdPrefix());
		String entryId = "" + data.getIdPrefix() + count;
		databaseConnection.setReference(databaseURL + databaseName + "/" + entryId);
		data.setID(entryId);
		databaseConnection.put(data.getJsonRepresentation());
		return "" + count;
	}
	
	public boolean createPatient(JSONPatient patient, String patientId)
	throws ResourceException {
		return createEntry(patient, "p"+patientId);
	}
	
	public boolean createAction(Action action, String actionId)
	throws ResourceException {
		return createEntry(action, "a"+actionId);
	}
	
	private boolean createEntry(CouchDBObject data, String entryId)
	throws ResourceException {
		incrementCount(data.getIdPrefix());
		databaseConnection.setReference(databaseURL + databaseName + "/" + entryId);
		data.setID(entryId);
		databaseConnection.put(data.getJsonRepresentation());
		return true;
	}
	
	public Action getAction(String actionId)
	throws ResourceException {
		try {
			Action action = new Action (getEntry("a"+actionId));
			action.remove(CouchDBObject.REV);
			action.put(CouchDBObject.ID, actionId);
			return action;
		}
		catch (IOException e)		{ System.out.println(e.toString()); }
		catch (JSONException e)		{ System.out.println(e.toString()); }
		return null;
	}

	/**
	 * @method getPatient 
	 * Retrieves a patient's information from the database.
	 * 
	 * @param patientId The location of the patient in the database.
	 * @return JSONPatient The patient's information in a JSONPatient object, or null on error.
	 * 
	 * @post Patient returned as JSONPatient or null on error.
	 * Warning: if there is an error in the database, there is no verification/recovery process in place.
	 */
	public JSONPatient getPatient(String patientId)
	throws ResourceException {
		try {
			JSONPatient patient = new JSONPatient(getEntry("p"+patientId));
			patient.remove(CouchDBObject.REV);
			patient.put(CouchDBObject.ID, patientId);
			return patient;
		}
		catch (IOException e)		{ System.out.println(e.toString()); }
		catch (JSONException e)		{ System.out.println(e.toString()); }
		return null;
	}

	private JsonRepresentation getEntry(String entryId)
	throws IOException, JSONException, ResourceException {
		databaseConnection.setReference(databaseURL + databaseName + 
				"/" + entryId + "?rev=" + getRevision(entryId));
		dataRep = databaseConnection.get();
		
		return new JsonRepresentation(dataRep);
	}

	/**
	 * @method deletePatient 
	 * Deletes a patient from the database.
	 * 
	 * @param patientId The location of the patient in the database.
	 * @return boolean Denotes whether the deletion was successful.
	 * 
	 * @post Patient deleted from database unless false was returned.
	 * Warning: if there is an error in the database, there is no verification/recovery process in place.
	 */
	public boolean deletePatient(String patientId)
	throws ResourceException {
		return deleteEntry("p"+patientId);
	}
	
	public boolean deleteAction(String actionId)
	throws ResourceException {
		return deleteEntry("a"+actionId);
	}
	
	private boolean deleteEntry(String entryId)
	throws ResourceException {
		String rev = getRevision(entryId);
		databaseConnection.setReference(databaseURL + databaseName +
				"/" + entryId + "?rev=" + rev);
		databaseConnection.delete();
		return true;
	}
	
	/**
	 * @method updatePatient 
	 * Updates a patient's information in the database.
	 * 
	 * @param patientId The location of the patient in the database.
	 * @param patient A (potentially incomplete) patient who's information will be applied to the patient at specified patientId in database.
	 * @return boolean Denotes whether the update was successful.
	 * 
	 * @post Patient updated in database unless false was returned.
	 * Warning: if there is an error in the database, there is no verification/recovery process in place.
	 */
	public boolean updatePatient(JSONPatient patient, String patientId)
	throws ResourceException {
		try						{ return updateEntry(patient, "p"+patientId); }
		catch (IOException e)	{ System.out.println(e.toString()); }
		catch (JSONException e)	{ System.out.println(e.toString()); }
		return false;
	}
	
	public boolean updateAction(Action action, String actionId)
	throws ResourceException {
		try						{ return updateEntry(action, "a"+actionId); }
		catch (IOException e)	{ System.out.println(e.toString()); }
		catch (JSONException e)	{ System.out.println(e.toString()); }
		return false;
	}
	
	private boolean updateEntry(CouchDBObject entry, String entryId)
	throws IOException, JSONException, ResourceException {
		try {
			CouchDBObject oldEntry;
			// Get revision. (Needed or a 409 error (Conflict) may occur.)
			String rev  = getRevision(entryId);
			// Set the reference to proper revision.
			databaseConnection.setReference(databaseURL + databaseName +
					"/" + entryId + "?rev=" + rev);
			// Get the patient (as a Representation).
			dataRep = databaseConnection.get();
	
			Class<? extends CouchDBObject> type = entry.getClass();
			Constructor<? extends CouchDBObject> g = type.getConstructor(new Class[] {Representation.class});
			oldEntry = g.newInstance(dataRep);
			
			//Key iterator, for updating only necessary fields.
			Iterator<String> it = entry.keyIter();
			while (it.hasNext()) {
				try {
					// Get next key.
					String key = it.next();
					// Get field at key (Throws exception if key has no value) 
					Object newData = entry.get(key);
					// Update our patient 
					oldEntry.put(key, newData);
				}
				catch (JSONException e) { continue; }
			}
			
			oldEntry.setRevision(rev);
			databaseConnection.put(oldEntry.getJsonRepresentation());
			return true;
		}
		catch (SecurityException e)			{ e.printStackTrace(); }
		catch (NoSuchMethodException e)		{ e.printStackTrace(); }
		catch (IllegalArgumentException e)	{ e.printStackTrace(); }
		catch (InstantiationException e)	{ e.printStackTrace(); }
		catch (IllegalAccessException e)	{ e.printStackTrace(); }
		catch (InvocationTargetException e)	{ e.printStackTrace(); }
		return false;
	}

	/**
	 * @method getAllPatients 
	 * Retrieves a patients' information from the database in an array.
	 * 
	 * @return JSONArray Contains JSONPatients built from all the information in the database.
	 * 
	 * @post Patients are returned as JSONArray or null on error.
	 * Warning: if there is an error in the database, there is no verification/recovery process in place.
	 */
	public JSONArray getAllPatients()
	throws ResourceException {
		try						{ return getAllEntries(JSONPatient.PREFIX); }
		catch (IOException e)	{ System.out.println(e.toString()); }
		catch (JSONException e)	{ System.out.println(e.toString()); }
		return null;
	}
	
	public JSONArray getAllActions()
	throws ResourceException {
		try						{ return getAllEntries(Action.PREFIX); }
		catch (IOException e)	{ System.out.println(e.toString()); }
		catch (JSONException e)	{ System.out.println(e.toString()); }
		return null;
	}
	private JSONArray getAllEntries( char prefix ) 
	throws IOException, JSONException, ResourceException {
		JSONArray ret = new JSONArray();
		databaseConnection.setReference(databaseURL + databaseName + "/" + "_all_docs");
		dataRep = databaseConnection.get();
		jsonObject = new JsonRepresentation(dataRep).getJsonObject();

		JSONArray directory = (JSONArray)jsonObject.get("rows");
		
		for(int i=0; i<directory.length(); i++) {
			String entryId = directory.getJSONObject(i).getString("id");
			if (entryId.charAt(0) != prefix || entryId.endsWith("counter"))
				continue;
			
			dataRep = getEntry(entryId);
			JSONObject entry = new JsonRepresentation(dataRep).getJsonObject();
        	entry.put(CouchDBObject.ID, entryId.substring(1));
        	ret.put(entry);
		}
		return ret;
		
	}

	public void purge(char type)
	throws ResourceException {
		String counterId = type + "counter";
		try {
			databaseConnection.setReference(databaseURL + databaseName + "/" + "_all_docs");
			dataRep = databaseConnection.get();
			jsonObject = new JsonRepresentation(dataRep).getJsonObject();
			
			JSONArray directory = (JSONArray)jsonObject.get("rows");
			
			for(int i=0; i<directory.length(); i++) { try {
				JSONObject entry = getEntry(directory.getJSONObject(i).getString("id")).getJsonObject();

				String entryId = (String) entry.get(CouchDBObject.ID);
				if (entryId.charAt(0) != type)
					continue;
	            if (entryId.substring(1).equals("counter"))
	            	continue;
	            
	            databaseConnection.setReference(databaseURL + databaseName
	            		+ "/" + entryId + "?rev=" + getRevision(entryId));
	            databaseConnection.delete();
			} catch (ResourceException e) { continue; } }
            databaseConnection.setReference(databaseURL + databaseName
            		+ "/" + counterId + "?rev=" + getRevision(counterId));
			dataRep = databaseConnection.get();
			jsonObject = new JsonRepresentation(dataRep).getJsonObject();
			jsonObject.put("value", "0");
			databaseConnection.put(new JsonRepresentation(jsonObject));
		}
		catch (IOException e)	{ System.out.println(e.toString()); }
		catch (JSONException e)	{ System.out.println(e.toString()); }
	}
	
	private String getRevision(String entryId)
	throws ResourceException {
		try {
			databaseConnection.setReference(databaseURL + databaseName + "/" + entryId); // generate URI for retrieval
			dataRep = databaseConnection.get(); // get resource
			// convert resource representation into a JSON object
			jsonObject = new JsonRepresentation(dataRep).getJsonObject();
			// get the revision field to delete the resource
			return jsonObject.getString(CouchDBObject.REV);
		}
		catch (IOException e)	{ System.out.println(e.toString()); }
		catch (JSONException e)	{ System.out.println(e.toString()); }
		return null;
	}
	
	private int incrementCount(char type)
	throws ResourceException {
		try {
			databaseConnection.setReference(databaseURL + databaseName + "/"+type+"counter");
			dataRep = databaseConnection.get();
			
			JSONObject counterObj = new JsonRepresentation(dataRep).getJsonObject();
			int dbCounter = counterObj.getInt("value");
			dbCounter++;
			counterObj.put("value", dbCounter);
			databaseConnection.put(new JsonRepresentation(counterObj));
			return dbCounter;
		}
		catch (IOException e)	{ System.out.println(e.toString()); }
		catch (JSONException e)	{ System.out.println(e.toString()); }
		return -1;
	}
}
//DO NOT DELETE; Reference code if ever needed
//	/**
//	 * @method createPatient
//	 * takes in a Patient object, creates a new Patient resource from that object, and uploads the resource to the database. 
//	 * The current implementation maintains a resource with a "_id" (an integer) provided by a counter resource.  
//	 * With each new resource, the counter is incremented by 1 and the result is the new resource's "_id".
//	 *
//	 * @param patient	the Patient object to be added to the database
//	 * @pre  patient is a valid Patient object with all fields initialized
//	 * @post if successful, a new Patient resource is added to the database; otherwise nothing happens
//	 * @returns the "_id" of the new resource, if it's successful; otherwise it returns null
//	 */
//	public String createPatient(Patient patient)
//	{
//		// initialize variables
//		System.out.println(databaseName);	
//		JSONObject doc = patient.getJSONObject();	// Patient object to be uploaded to the database
//		System.out.println( doc.toString() );
//		
//		try
//		{
//			// get JSON object to create the new Patient resource
//			// get next "_id" for new Patient resource:
//			databaseResource.setReference(databaseURL + databaseName + "/" + "counter"); // generate URI for retrieval
//			System.out.println( databaseResource.getReference().toString() );
//			dataRep = databaseResource.get(); // get counter resource
//			// convert resource's representation into a JSON object
//			jsonRep = new JsonRepresentation(dataRep);
//			JSONObject tempobj = jsonRep.getJsonObject();
//			// update counter (new Patient "_id")
//			Long value = new Long( Long.parseLong( tempobj.getString("value")));
//			value++;
//			System.out.println("value " + value.toString());
//			tempobj.put("value", value.toString()); 
//			
//			/* This is required to prevent the server from timing out while servicing requests.
//			 * This only happens occasionally. */
//			databaseResource.setNext(new Client(Protocol.HTTP));
//			
//			// create a new Patient resource
//			databaseResource.setReference(databaseURL + databaseName); // generate URI for creation
//			doc.put("_id", value.toString());
//			dataRep = databaseResource.post(new JsonRepresentation(doc)); // uploads resource to database
//			jsonRep = new JsonRepresentation(dataRep);
//			jsonObject = jsonRep.getJsonObject();
//			
//			databaseResource.setNext(new Client(Protocol.HTTP));
//			
//			// since resource was successfully created, update the counter 
//			databaseResource.setReference(databaseURL + databaseName + "/" + "counter"); // generate URI for updating
//			dataRep = databaseResource.put(new JsonRepresentation(tempobj)); // updates counter
//			jsonRep = new JsonRepresentation(dataRep);
//			jsonObject = jsonRep.getJsonObject();
//			
//			return value.toString(); // "_id" of new resource
//		}
//		// error when accessing the database or a resource in it
//		catch(ResourceException e) { e.printStackTrace(); }
//		// error when manipulating the JSON object
//		catch(JSONException e) { e.printStackTrace(); }
//		// error when creating the JSON object
//		catch(IOException e) { e.printStackTrace(); }		
//		return null;
//	}
//
//	private JSONArray getAllEntries( Class<? extends CouchDBObject> type )
//	throws IOException, JSONException, ResourceException {
//		JSONArray ret = null;
//		databaseConnection.setReference(databaseURL + databaseName + "/" + "_all_docs");
//		dataRep = databaseConnection.get();
//		jsonObject = new JsonRepresentation(dataRep).getJsonObject();
//		try {
//			JSONArray directory = (JSONArray)jsonObject.get("rows");
//			//First class functions anyone? Served with a hot arm ready to throw...
//			ArrayList<CouchDBObject> entries = new ArrayList<CouchDBObject>();
//			Constructor<? extends CouchDBObject> g =
//				type.getConstructor(new Class[] {Representation.class});
//			Constructor<? extends CouchDBObject> h =
//				type.getConstructor(new Class[] {});
//			char prefix = h.newInstance().getIdPrefix();
//	
//			for(int i=0; i<directory.length(); i++) {
//				String entryId = directory.getJSONObject(i).getString("id");
//				if (entryId.charAt(0) != prefix)
//					continue;
//				
//				dataRep = getEntry(entryId);
//				CouchDBObject entry = g.newInstance(dataRep);
//				
//	            if ( ((String)entry.get(CouchDBObject.ID)).substring(1).equals("counter") )
//	            	continue;
//	        	entry.put(CouchDBObject.ID, entryId.substring(1));
//			}
//			ret = new JSONArray(entries);
//		}
//		catch (SecurityException e)			{ e.printStackTrace(); }
//		catch (NoSuchMethodException e)		{ e.printStackTrace(); }
//		catch (IllegalArgumentException e)	{ e.printStackTrace(); }
//		catch (InstantiationException e)	{ e.printStackTrace(); }
//		catch (IllegalAccessException e)	{ e.printStackTrace(); }
//		catch (InvocationTargetException e)	{ e.printStackTrace(); }
//		return ret;
//	}
//
//	/**
//	 * @method getAllPatients
//	 * retrieves all patient resources in the database.
//	 * The special document which stores the counter value is ignored and is not returned.
//	 *
//	 * @pre none
//	 * @returns an array of JSON objects as long as there is at least one resource in the database;
//	 *			otherwise it returns null
//	 */
//	public JSONArray getAllPatients()
//	{
//		try
//		{
//			databaseResource.setReference(databaseURL + databaseName + "/" + "_all_docs"); // generate URI for retrieval
//			dataRep = databaseResource.get(); // get resources
//			// convert resources' representations into JSON objects
//			jsonRep = new JsonRepresentation(dataRep);
//			jsonObject = jsonRep.getJsonObject();
//			return (JSONArray)jsonObject.get("rows");
//		}
//		catch(ResourceException e) { // error when accessing the database or a resource in it
//			e.printStackTrace();
//		} catch(JSONException e) { // error when manipulating the JSON object
//			e.printStackTrace();
//		} catch(IOException e) { // error when creating the JSON object
//			e.printStackTrace();
//		}
//		return null;
//	}
//	
//	//copied and edited from deletePatient
//	public boolean deleteTransition(String id)
//	{
//		try
//		{
//			databaseResource.setReference(databaseURL + databaseName + "/" + id); // generate URI for retrieval
//			dataRep = databaseResource.get(); // get resource
//			// convert resource representation into a JSON object
//			jsonRep = new JsonRepresentation(dataRep);
//			jsonObject = jsonRep.getJsonObject();
//			// use the revision field to delete the resource
//			String rev  = jsonObject.getString("_rev");
//			databaseResource.setReference(databaseURL + databaseName + "/" + id + "?rev=" + rev);
//			dataRep = databaseResource.delete();
//			return true;
//		}
//		catch(ResourceException e) { // error when accessing the database or a resource in it
//			e.printStackTrace();
//		} catch(JSONException e) { // error when manipulating the JSON object
//			e.printStackTrace();
//		} catch(IOException e) { // error when creating the JSON object
//			e.printStackTrace();
//		}
//		return false;
//	}
